package panda.server;

import java.io.File;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import panda.client.engine.CounterAndPos;
import panda.client.engine.MagicCard;
import panda.client.engine.MagicCardAndPos;
import panda.client.engine.MagicCardDB;
import panda.client.engine.MagicCardToken;
import panda.client.engine.ServerModel;
import panda.client.enumerations.ClientID;
import panda.client.enumerations.MagicSet;
import panda.client.services.DataService;

import com.google.gwt.user.server.rpc.RemoteServiceServlet;

public class DataServiceImpl extends RemoteServiceServlet implements DataService {
	private static final boolean DEBUG = true;
	private static final long serialVersionUID = 1L;
	private final Hashtable<ClientID, Boolean> clientStates = new Hashtable<ClientID, Boolean>();

	private List<MagicCard> serverCardDB = new ArrayList<MagicCard>();
	private Long serverID = 1L;

	private ServerModel model;
	private long startTimestamp = System.currentTimeMillis();
	private int clientsRegistered = 0;
	private ClientID lastClientWhoUpdated = null;
	private long modelNumber;

	public static final String kyruruDotNetRoot = "webapps/cardpanda/decks";   /* for personal tomcat server */
	/* /home/kyruru/public_html/cardpanda/decks */   /* change to zcp if 2nd cp copy */

	public static final String hostedModeRoot = "c:\\Idea\\beta-cardpanda\\src\\panda\\public\\decks";

    // public static final String hostedModeRoot = "Cardpanda/src/panda/public/decks/";
	
	private static final boolean ObserverFirsthostedMode = true; 
	// private List<MagicCard> cardDB;

	private static String nameOfLogger = DataServiceImpl.class.getName();
	private static Logger log = Logger.getLogger(nameOfLogger);

	public DataServiceImpl() {

		try
		{
			File f = new File("hello.txt");
			System.out.println(f.getCanonicalPath());
		}
		catch (Exception e)
		{
			e.printStackTrace();
		}

		System.out.println("Loading at " + getTimestamp() + "ms");

		serverCardDB.addAll(MagicCardDB.getCardsFromSet(MagicSet.ALARA_REBORN));
		serverCardDB.addAll(MagicCardDB.getCardsFromSet(MagicSet.ALARA_REBORN_TOKENS));
		serverCardDB.addAll(MagicCardDB.getCardsFromSet(MagicSet.CONFLUX));
		serverCardDB.addAll(MagicCardDB.getCardsFromSet(MagicSet.CONFLUX_TOKENS));
		serverCardDB.addAll(MagicCardDB.getCardsFromSet(MagicSet.SHARDS_OF_ALARA));
		serverCardDB.addAll(MagicCardDB.getCardsFromSet(MagicSet.SHARDS_OF_ALARA_TOKENS));
		serverCardDB.addAll(MagicCardDB.getCardsFromSet(MagicSet.EVENTIDE));
		serverCardDB.addAll(MagicCardDB.getCardsFromSet(MagicSet.EVENTIDE_TOKENS));
		serverCardDB.addAll(MagicCardDB.getCardsFromSet(MagicSet.SHADOWMOOR));
		serverCardDB.addAll(MagicCardDB.getCardsFromSet(MagicSet.SHADOWMOOR_TOKENS));
		serverCardDB.addAll(MagicCardDB.getCardsFromSet(MagicSet.MORNINGTIDE));
		serverCardDB.addAll(MagicCardDB.getCardsFromSet(MagicSet.MORNINGTIDE_TOKENS));
		serverCardDB.addAll(MagicCardDB.getCardsFromSet(MagicSet.LORWYN));
		serverCardDB.addAll(MagicCardDB.getCardsFromSet(MagicSet.LORWYN_TOKENS));

		serverCardDB.addAll(MagicCardDB.getCardsFromSet(MagicSet.M_10));
		serverCardDB.addAll(MagicCardDB.getCardsFromSet(MagicSet.M_10_TOKENS));
		
		// serverCardDB.addAll(MagicCardDB.getCardsFromSet(MagicSet.TENTH_EDITION));
		// serverCardDB.addAll(MagicCardDB.getCardsFromSet(MagicSet.TENTH_EDITION_TOKENS));

		System.out.println("Done in: " + getTimestamp() + "ms");

		model = new ServerModel();
		model.setP1Life(20);
		model.setP2Life(20);
		model.setInPlay(new ArrayList<MagicCardAndPos>());
		model.setCounters(new ArrayList<CounterAndPos>());
		model.setP1gy(new ArrayList<MagicCard>());
		model.setP1hand(new ArrayList<MagicCard>());
		model.setP1lib(new ArrayList<MagicCard>());
		model.setP2gy(new ArrayList<MagicCard>());
		model.setP2hand(new ArrayList<MagicCard>());
		model.setP2lib(new ArrayList<MagicCard>());
		model.setUpdateMsg("Server has just started (or reset).");
		model.setModelNum(0);

		modelNumber = 0;
		loadDecks();
	}

	private long getTimestamp() {
		return System.currentTimeMillis() - startTimestamp + 10000000;
	}

	/**
	 * Generates a unique sessionID and Boolean hasChanged. Puts (sessionID, hasChanged) pair into server's HashTable.
	 * HasChanged is true because the client needs an initial update its model from the server.
	 */
	public synchronized ClientID registerClient(String placeholder /* will be used to let client pick his name */) {
		ClientID clientSessionID;
		clientsRegistered++;

		/**
		 * if we're in hosted mode, make the first client that logs in the observer. otherwise, first client is P1 and
		 * second client is P2
		 */
		if(new File(DataServiceImpl.kyruruDotNetRoot).exists()) {
			switch(clientsRegistered) {
				case 1:
					clientSessionID = ClientID.P1;
					break;
				case 2:
					clientSessionID = ClientID.P2; // sessionIDs[0];
					break;
				case 3:
					clientSessionID = ClientID.OBSERVER; // sessionIDs[1];
					break;
				default:
					clientSessionID = ClientID.OBSERVER; // sessionIDs[2];
			}
		} else if(new File(DataServiceImpl.hostedModeRoot).exists()) {
			if(ObserverFirsthostedMode) {
				switch(clientsRegistered) {
					case 1:
						clientSessionID = ClientID.OBSERVER;
						break;
					case 2:
						clientSessionID = ClientID.P1; // sessionIDs[0];
						break;
					case 3:
						clientSessionID = ClientID.P2; // sessionIDs[1];
						break;
					default:
						clientSessionID = ClientID.OBSERVER; // sessionIDs[2];
				}
			} else {
				switch(clientsRegistered) {
					case 1:
						clientSessionID = ClientID.P1; // sessionIDs[0];
						break;
					case 2:
						clientSessionID = ClientID.P2; // sessionIDs[1];
						break;
					case 3:
						clientSessionID = ClientID.OBSERVER;
						break;
					default:
						clientSessionID = ClientID.OBSERVER; // sessionIDs[2];
				}				
			}
		} else {
			throw new IllegalStateException("Neither kyruru.net nor hosted mode were found.");
		}

		clientStates.put(clientSessionID, Boolean.TRUE);

		if(DEBUG) {
			System.out.println(getTimestamp() + " [s:registerClient] " + clientSessionID + " registered");
		}

		return clientSessionID;
	}

	public synchronized ServerModel getModel(ClientID sessionID) {
		if(DEBUG) {
			System.out.println(getTimestamp() + " [s:getModel]    " + sessionID + " checking for updates");
		}

		while(clientStates.get(sessionID) == Boolean.FALSE) {
			try {
				if(DEBUG) {
					System.out.println(getTimestamp() + " [s:getModel]    ... and " + sessionID + " is waiting.");
				}

				wait();
			} catch(InterruptedException e) {
				e.printStackTrace();
			}
		}

		clientStates.put(sessionID, Boolean.FALSE);

		if(DEBUG) {
			System.out.println(getTimestamp() + " [s:getModel]    model has changed for " + sessionID
					+ ".  Returning model to " + sessionID);
		}

		/**
		 * Code no longer exists: Gets only relevant calendars from the server. We truncate any calendars not being
		 * listened to by the client before sending back the (probably smaller) model.
		 */
		return model;
	}

	/**
	 * A client calls this method to update the server's data model. Changes all clientState values to Boolean.TRUE and
	 * notifies all waiting threads.
	 */
	public synchronized void updateModel(ClientID sessionID, ServerModel model) {
		/**
		 * prevents clients from different sessions from updating a freshly restarted server cardpanda session: ignores
		 * their requests
		 */
		if(model.getModelNum() != 0 && this.lastClientWhoUpdated == null) {
			log.info("client " + sessionID
					+ " is trying to update the server after it restarted recently. Ignoring request");
		} else {

			if(DEBUG) {
				// System.out.println(getTimestamp() + " [s:updateModel] model is being updated by [" + sessionID + " from "
				// + this.model.toString() + " to " + model.toString());
				log.info(sessionID + " is updating the server's model");
				System.out.println(getTimestamp() + " [s:updateModel] model is being updated by [" + sessionID + "]");
				System.out.println(getTimestamp() + " [s:updateModel] updater: [" + sessionID + "] (server#: "
						+ modelNumber + ", client#: " + model.getModelNum() + "). Last client who updated: "
						+ lastClientWhoUpdated);
				System.out.println(getTimestamp() + " [s:updateModel] update msg: " + model.getUpdateMsg());
			}

			/**
			 * Update the server copy of the model if: <br>
			 * 1. the model numbers match<br>
			 * 2. it's the same client updating as before<br>
			 * <br>
			 * There is no need to check for concurrent modification errors if it's the same client making many rapid
			 * changes, like incrementing life total or moving several cards around.
			 */
			if(model.getModelNum() == modelNumber || this.lastClientWhoUpdated.equals(sessionID)) {
				this.model = model;
				this.lastClientWhoUpdated = sessionID;

				/* the model has changed so we increment its model number */
				modelNumber++;
				this.model.setModelNum(modelNumber);

				/* change all clientState values to Boolean.TRUE except the client that just updated */
				Set<ClientID> keys = clientStates.keySet();
				for(ClientID ID : keys) {
					if(!sessionID.equals(ID)) {
						clientStates.put(ID, Boolean.TRUE);
					}
				}
				notifyAll();

				if(DEBUG) {
					System.out.println(getTimestamp() + " [s:updateModel] notifying all threads after model update from "
							+ sessionID);
				}
			} else { /* if the modelnumber doesn't match */

				/* update the model's updateMsg to let the user know his action failed */
				this.model.setUpdateMsg("Your opponent was moving something so your latest action was discarded =(");

				clientStates.put(sessionID, Boolean.TRUE);
				notifyAll();

				if(DEBUG) {
					System.out
							.println(getTimestamp()
									+ " [s:updateModel] notifying "
									+ sessionID.getName()
									+ " that its last action was undone due to concurrent modification.  Server received model number "
									+ model.getModelNum() + " but expected " + this.model.getModelNum());
				}
			}
		}
	}

	/**
	 * Linear search of all cards in a list for a card of the exact name. Returns the first match. Throws
	 * NoSuchElementException if card isn't found.
	 */
	@Deprecated
	public MagicCard getCardViaExactName(String name) {
		MagicCard result = null;
		for(MagicCard card : serverCardDB) {
			if(card.getName().toLowerCase().equals(name.toLowerCase())) {

				/* add a -copy- of the card so setServerID doesn't set multiple copies of the same card to the same ID */
				result = new MagicCard(card.getMagicSet(), card.getName(), card.getCastingCost(), card.getType(), card
						.getBodyText(), card.getRarity(), card.getIllustrator(), card.getID());

				synchronized(serverID) {
					result.setServerID(serverID++);
				}

				break;
			}
		}

		if(result == null) {
			throw new NoSuchElementException(name + " not found in collection");
		}

		return result;
	}

	/**
	 * WARNING: This method does not set serverIDs. Linear search of all cards in a list for a card whose name matches
	 * the pattern. Returns a list of cards which can be empty.
	 */
	@Deprecated
	public List<MagicCard> getCardViaRegexName(String pattern) {
		List<MagicCard> results = new ArrayList<MagicCard>();
		Pattern p = Pattern.compile(".*" + pattern.toLowerCase() + ".*");

		for(MagicCard card : serverCardDB) {
			Matcher m = p.matcher(card.getName().toLowerCase());
			if(m.matches()) {

				/* add a -copy- of the card so setServerID doesn't set multiple copies of the same card to the same ID */
				MagicCard result = new MagicCard(card.getMagicSet(), card.getName(), card.getCastingCost(), card.getType(),
						card.getBodyText(), card.getRarity(), card.getIllustrator(), card.getID());
				results.add(result);

				synchronized(serverID) {
					result.setServerID(serverID++);
				}
			}
		}

		return results;
	}

	/**
	 * WARNING: This method does not set serverIDs. Linear search of all cards in a list for a card whose text matches
	 * the pattern. This includes name, casting cost, type, and bodyText. Pattern can be delimited by space and this
	 * method will only return cards matching all tokens. Returns a list of cards which can be empty.
	 */
	@Deprecated
	public List<MagicCard> getCardViaRegexText(String pattern) {
		System.out.println("In getCardViaRegexText: " + (System.currentTimeMillis() - startTimestamp) + "ms");
		List<MagicCard> results = new ArrayList<MagicCard>();
		Pattern p;

		String[] tokens = pattern.split(" ");
		System.out.print("Looking for terms: ");
		for(String token : tokens) {
			System.out.print("." + token);
		}
		System.out.println(".");

		boolean matchesAllTermsSoFar;

		for(MagicCard card : serverCardDB) {
			matchesAllTermsSoFar = true;

			for(String token : tokens) {
				p = Pattern.compile(".*" + token.toLowerCase() + ".*");
				Matcher name = p.matcher(card.getName().toLowerCase());
				Matcher castingCost = p.matcher(card.getCastingCost().toLowerCase());
				Matcher type = p.matcher(card.getType().toLowerCase());
				Matcher bodyText = p.matcher(card.getBodyText().toLowerCase());
				Matcher illustrator = p.matcher(card.getIllustrator().toLowerCase());

				if(name.matches() || castingCost.matches() || type.matches() || bodyText.matches() || illustrator.matches()) {
					// if any of the text in the card matches
				} else {
					matchesAllTermsSoFar = false;
					break;
				}
			}

			if(matchesAllTermsSoFar) {
				/* add a -copy- of the card so setServerID doesn't set multiple copies of the same card to the same ID */
				MagicCard result = new MagicCard(card.getMagicSet(), card.getName(), card.getCastingCost(), card.getType(),
						card.getBodyText(), card.getRarity(), card.getIllustrator(), card.getID());
				results.add(result);

				synchronized(serverID) {
					result.setServerID(serverID++);
				}
			}
		}

		System.out.print("results: ");
		for(MagicCard card : results) {
			System.out.print(" '" + card.getName() + "'");
		}
		System.out.println(".");

		System.out.println("Out of getCardViaRegexText: " + (System.currentTimeMillis() - startTimestamp) + "ms");
		System.out.println(results.size() + " cards found with pattern: " + pattern);
		return results;
	}

	/**
	 * Grabs a card by set and id from the server - a linear operation wrt number of total cards in the DB. Also matches
	 * name, but name technically would not have been necessary to implement this function. As it is, it's required and
	 * an included argument mostly for ease of code reading. Throws NoSuchElementException if card isn't found. Cards are
	 * unique by set and id, so this stops when it finds the first match; there shouldn't be any other matches.
	 */
	public MagicCard getCardViaExactID(String name, MagicSet set, int id) {
		// System.out.println("Entering getCardViaExactID at " + (System.currentTimeMillis() - appStartTime) + "ms.
		// serverID=" + serverID);

		MagicCard result = null;
		for(MagicCard card : serverCardDB) {
			if(card.getName().toLowerCase().equals(name.toLowerCase()) && card.getMagicSet().equals(set)
					&& card.getID() == id) {

				/* add a -copy- of the card so setServerID doesn't set multiple copies of the same card to the same ID */
				/* also: make sure tokens remain tokens so we can differentiate them from normal MagicCards on the client */
				if(card instanceof MagicCardToken) {
					result = new MagicCardToken(card.getMagicSet(), card.getName(), card.getCastingCost(), card.getType(),
							card.getBodyText(), card.getRarity(), card.getIllustrator(), card.getID());
				} else {
					result = new MagicCard(card.getMagicSet(), card.getName(), card.getCastingCost(), card.getType(), card
							.getBodyText(), card.getRarity(), card.getIllustrator(), card.getID());
				}

				synchronized(serverID) {
					result.setServerID(serverID++);
				}

				break;
			}
		}

		if(result == null) {
			throw new NoSuchElementException(name + " not found in collection");
		}

		// System.out.println("Leaving getCardViaExactID at " + (System.currentTimeMillis() - appStartTime) + "ms.
		// resultID=" + result.getServerID() + " but the server's ID is " + serverID);
		return result;
	}

	/** returns a new, unique serverID */
	public synchronized long getNewServerID() {
		if(DEBUG) {
			System.out.println(getTimestamp() + " [s:getNewServerID] is returning a new serverID: " + serverID);
		}

		return serverID++;
	}

	private void loadDecks() {
		// loadDeckToClientLibrary(ClientID.P1, DeckDB.getDeckList(Deck.OWEN));
		// loadDeckToClientLibrary(ClientID.P2, DeckDB.getDeckList(Deck.ZANE));
		DeckScanner scanner = new DeckScanner(serverCardDB);

		loadDeckToClientLibrary(ClientID.P1, scanner.assembleDecklistFromFile("p1deck.txt"));
		loadDeckToClientLibrary(ClientID.P2, scanner.assembleDecklistFromFile("p2deck.txt"));
	}

	/**
	 * Loads a decklist into a client's library.
	 * 
	 * @param name
	 * @param decklist
	 */
	private void loadDeckToClientLibrary(ClientID name, List<DeckEntry> decklist) {
		List<MagicCard> library;

		switch(name) {
			case P1:
				library = model.getP1lib();
				break;
			case P2:
				library = model.getP2lib();
				break;
			default:
				throw new IllegalArgumentException("Must be either P1 or P2");
		}

		for(DeckEntry entry : decklist) {
			MagicSet targetSet = null;
			/* this loop to match set abbreviation to a set is preeeeeety inefficient. */
			for(MagicSet set : MagicSet.values()) {
				if(set.getAbbrev().equals(entry.getMagicSetAbbrev())) {
					targetSet = set;
				}
			}

			assert targetSet != null : "Set " + entry.getMagicSetAbbrev() + " not found";

			/* add as many copies of the card as specified by the entry */
			for(int i = 0; i < entry.getCopies(); i++) {
				library.add(getCardViaExactID(entry.getCardName(), targetSet, entry.getID()));
			}
		}
	}
}